当前位置:PHP
原文作者:ANANT GARG
译者的话:
本节是《写一个自己的PHP MVC 框架》的第2节,在本节中,MVC框架和结构做了不少变动,同时没有在文章中贴出所有代码,所以,阅读之前,请先下载文章代码。原文代码下载链接
简介
在本节中,我们开发了一个简单的电子商务网站,包含目录、子目录、商品以及商品的标签。该示例网站将帮助你了解由本节框架提供的各种表和新的功能之间的关系。
新的变化
config/inflection.php
inflection.php
使我们能够在数据库中使用不规则单词(单词的复数形式不是直接加s的单词)名称的表名,同时该文件被 library/inflection.class.php
文件所引用。
译者的话:
inflection.php文件中所定义的名称为
$irregularWords
的数组为空,如果后期你在基于该MVC框架进行网站开发时,用到了在library/inflection.class.php
文件中没有包含的不规则的单词作为表名,那么你便可以将你的表名填入该数组。
config/routing.php
我们通过routing.php
文件定义默认的控制器和动作,同时,使用正则表达式自定义了一条重定向规则。当前仅定义了一个重定向,通过该规则,像http://localhost/admin/categories/view
这样的URL将变成http://localhost/admin/categories_view
后传递给定义的全局变量$url
。其中admin
是控制器,categories_view
是动作。通过重定向使我们可以创建一个易于seo的管理后台URL。你可以根据自己的需要定义其它的重定向规则。
library/cache.class.php
cache
类只有一些简单的内容,当前仅有2个简单的功能 - set 和 get 。数据存储在 cache 目录下的富文本文件中。cache 函数当前仅仅用来描述数据表的字段信息。
library/html.class.php
HTML 类用作 template 类的辅助类。该类使你能够用少量的标准函数创建链接、添加 javascript 和 css。我已经添加了一个函数将链接转换为微型的URL。该类仅用在视图中,例如:$html->includeJs(‘generic.js’)
library/inflection.class.php
上一节中,仅仅是通过在单词后加’s’的方式将该单词变成复数。然而,为了更有弹性,现在我们使用由 Sho Kuwamoto 所写的inflection
类进行盲改。看过这个类就会知道,它使用了一些简单的正则表达式。当然,如果将来这个类中能添加一个缓存功能就更好了。
library/template.class.php
我添加了一个名叫doNotRenderHeader
的新变量,它可以使你能在一些特殊场合(action)下不输出头文件。它可以用于AJAX调用并且不需要返回头文件时的情形。默认它已经被控制器调用了。e.g. $this->doNotRenderHeader = 1
。
library/vanillacontroller.class.php &vanillamodel.class.php
除了加了一个 vanilla 前缀强调它的简单性外,完全没有改变。
library/sqlquery.class.php
SQLQuery类是整个框架的核心。该类使你可以将你的数据表当作对象使用。
先来了解下一些简单的功能吧 - save() 和 delete()
save()函数必须从controller调用。save()函数有2个选项 - 如果 id 已经设置,那么它将更新这条记录。如果没有设置,它将创建一条新的记录。以categories类为例 application/controllers/categoriescontroller.php
。我们的函数像下面这样:
function new() {
$this->Category->id = $_POST['id'];
$this->Category->name = $_POST['name'];
$this->Category->save();
}
如果$this->Category->id = null
,那么将在 categories 表创建一条新的记录。
delete()函数用于删除表中的记录。样例如下:
function delete($categoryId) {
$this->Category->id = $categoryId;
$this->Category->delete();
}
尽管你会使用 POST 代替 GET 进行删除记录的查询操作。但是为了使操作具有幂等性还是应该使用GET。幂等操作意味着不会改变数据库的状态(换句话说,就是不会 update/delete/insert 行。简言之,就是数据库不会有任何改变)
下面看下search()函数。我知道这个函数猛然间看到会感到有点复杂。但是在我们把它分解后,你会看到实际上非常直白。如果你对数据库表的关系(1对1,1对多,多对多)还不是很了解的话,可以看这里 。在设计数据库时,我们使用以下约定:
1对1 关系
对于1对1关系的表而言,假设我们的2个表名字是students和memtors,每个学生只有一个导师。那么在students表中应该有一个字段名为’mentor_id’,该字段与mentors表中字段名’id’一致。
1对多 关系
就1对多关系而言,假设我们的2个表名字是parents和children,每个父母可以有多个孩子。那么在children表中应该有一个字段名为’parent_id’,该字段与parents表中字段名’id’一致。
多对多 关系
对于多对多关系而言,假设我们的2个表名字是students和teachers,学生与老师之间存在多对多的关系。那么我们应该创建一个名字为students_teachers
的新表。这个新表应该有三个字段分别是: id,student_id,teacher_id
,新表的命名规则以字母顺序进行命名。举例来说,如果2个基表是cars和animals,复合表的名字应该是animals_cars
,而非cars_animals
。
Now,如果我们已经按约定规则创建好了数据库,那么我们必须要告诉我们的框架这个数据库的存在啊。让我们看下 models/product.php。
class Product extends VanillaModel {
var $hasOne = array('Category' => 'Category');
var $hasManyAndBelongsToMany = array('Tag' => 'Tag');
}
第一个Category是一个别名,第二个Category才是实际的模型名字。多数情况下,这两者名字是相同的。现在让我们看下models/category.php的不同之处。
<?php
class Category extends VanillaModel {
var $hasMany = array('Product' => 'Product');
var $hasOne = array('Parent' => 'Category');
}
在这里,每个category有一个父级category。因此,当model是Category时别名是Parent;因此,在categories表中应该有一个名字为parent_id的字段。为了更好的理解他们之间的关系,我建议你创建一对表,然后测试输出,看看结果。在你的controller中代码像这样:
function view() {
$this->Category->id = 1;
$this->Category->showHasOne();
$this->Category->showHasMany();
$this->Category->showHMABTM();
$data = $this->Category->search();
print_r($data);
}
好了,现在让我们试着理解search()函数。如果他们之间没有关系,那么函数简单的做一个查询所有的操作即可select * from tableName
(tableName和控制器名字相同)。我们可以使用下面的命令来影响数据数据结果:
where(‘fieldName’,’value’) => Appends WHERE ‘fieldName’ = ‘value’
like(‘fieldName’,’value’) => Appends WHERE ‘fieldName’ LIKE ‘%value%’
setPage(‘pageNumber’) => Enables pagination and display only results for the set page number
setLimit(‘fieldName’,’value’) => Allows you to modify the number of results per page if pageNumber is set. Its default value is the one set in config.php.
orderBy(‘fieldName’,’Order’) => Appends ORDER BY ‘fieldName’ ASC/DESC
id = X => Will display only a single result of the row matching the id
现在假设showHasOne()函数被调用,那么对于每个hasOne关系,相当于做了一次左外连接查询。(代码位置:sqlquery.class.php 91 - 99 行)
if ($this->_hO == 1 && isset($this->hasOne)) {
foreach ($this->hasOne as $alias => $model) {
$table = strtolower($inflect->pluralize($model));
$singularAlias = strtolower($alias);
$from .= 'LEFT JOIN `'.$table.'` as `'.$alias.'` ';
$from .= 'ON `'.$this->_model.'`.`'.$singularAlias.'_id` = `'.$alias.'`.`id` ';
}
}
现在假设showHasMany()函数被调用,那么对于上面查询返回的结果以及每个hasMany关系,它将找出在第二张表中的所有匹配当前结果id的记录(代码位置:sqlquery.class.php 150 行)。然后它将所有数据放入相同的数组。例如,如果teaches有多个studnets,那么$this->Teacher->showHasMany()
将会在students表中搜索字段为teacher_id的记录。
最后,如果showHMABTM()函数被调用,那么对于第一个查询所返回的结果以及每个hasManyAndBelongsToMany关系,它将找出所有与当前结果id匹配的记录(代码位置:sqlquery.class.php 201 行)。例如,如果teaches与students之间存在多对多关系,那么 $this->Teacher->showHMABTM()
将在studnets_teachers
表中查找字段名为teacher_id
的记录。
在sqlquery.class.php 236 行,如果设置了id,那么将返回单条记录(并且不是数组),反之,则返回一个数组。然后该函数会调用clear()函数重置所有变量(代码位置:sqlquery.class.php 368-380 行)
在商品页,假设有这样一个分页方式。在controllers/productscontroller.php 文件中的代码应该想下面这样:
function page ($pageNumber = 1) {
$this->Product->setPage($pageNumber);
$this->Product->setLimit('10');
$products = $this->Product->search();
$totalPages = $this->Product->totalPages();
$this->set('totalPages',$totalPages);
$this->set('products',$products);
$this->set('currentPageNumber',$pageNumber);
}
那么与此同步的视图文件 views/products/page.php的代码应该像这样:
<?php foreach ($products as $product):?>
<div>
<?php echo $product['Product']['name']?>
</div>
<?php endforeach?>
<?php for ($i = 1; $i <= $totalPages; $i++):?>
<div>
<?php if ($i == $currentPageNumber):?>
<?php echo $currentPageNumber?>
<?php else: ?>
<?php echo $html->link($i,'products/page/'.$i)?>
<?php endif?>
</div>
<?php endfor?>
仅仅用了几行代码我们就启用了商品页的分页功能,并且URLS也很友好 (/products/page/1, products/page/2 …)
如果你已经看了位于第384-397行的 totalPages()函数,那么你应该已经知道它使用了一个已经存在的查询去除了LIMIT条件来返回总数。这个count/limit 提供给我们分页的总数。
现在假设你在categories控制器下想使用查询来查询products表。一种方法是应用自定义的查询语句$this->Category->custom(‘select * from products where ….’)
;另外,你也可以使用performAction()函数(第49-57行)来调用其它控制器的某个动作。一个在categoriescontroller.php 文件中的调用例子如下:
function view($categoryId = null, $categoryName = null) {
$categories = performAction('products','findProducts',array($categoryId,$categoryName));
}
与此相对应的productscontroller.php的代码如下:
function findProducts ($categoryId = null, $categoryName = null) {
$this->Product->where('category_id',$categoryId);
$this->Product->orderBy('name');
return $this->Product->search();
}
上面的内容简要的介绍了SQLQuery类的所有功能,你可以非常容易的根据自己的需要进行扩展。例如,对结果进行缓存,对hasMany,hasOne,hasMABTM查询添加额外的条件等。
对于用户注册功能,我通过定义beforeAction和afterAction函数打了一个基础,这两个函数会在每个控制动作发生前被执行。它可以使我们通过调用一个函数来检查用户cookie是否已设置,同时是否是一个有效用户。
我要实现的功能是一个商品的管理平台。为此我创建了一个名为 models/admin.php的无用模型,还设置了一个名为$abstract=true的变量。它将告诉我们的VanillaModel 模型不去数据库中查找与其相关联的表。如前所述,我创建的路由策略是将 admin/X/Y 转变为admin/X_Y
。因此像 admin/categories/delete/15 这样的URLS实际上将调用controllers/admincontroller.php文件中的 categories_delete(15)
函数。
千知博客